#include "Model.h"

#include <fstream>
#include <iostream>
#include <map>

using std::map;

Model::Model(const string& filename) :
m_filename(filename)
{
    if (!loadObject())
    {
        std::cerr << "Unable to load object: " << m_filename << std::endl;
    }
    else
    {
        computeTangentBasis();
        indexVBO();
        m_bufferObject.size = static_cast<GLsizei>(m_indices.size());
        setVertexAttributes();
    }

    m_vertices.clear();
    m_normals.clear();
    m_uvs.clear();
    m_tangents.clear();
    m_bitangents.clear();
    m_indices.clear();
}

Model::~Model()
{
    glDeleteBuffers(1, &m_bufferObject.vertex);
    glDeleteBuffers(1, &m_bufferObject.normal);
    glDeleteBuffers(1, &m_bufferObject.uv);
    glDeleteBuffers(1, &m_bufferObject.tangent);
    glDeleteBuffers(1, &m_bufferObject.bitangent);
    glDeleteBuffers(1, &m_bufferObject.element);
    glDeleteVertexArrays(1, &m_bufferObject.vao);
}

GLuint Model::getVAO() const
{
    return m_bufferObject.vao;
}

string Model::getName() const
{
    return m_filename;
}
GLsizei Model::getBufferObjectSize() const
{
    return m_bufferObject.size;
}

bool Model::loadObject()
{
    string filepath = "Models/" + m_filename + ".obj";
    std::ifstream objFile(filepath.c_str());

    if (!objFile.is_open())
    {
        std::cerr << "Unable to open " << m_filename << std::endl;
        return false;
    }

    vector<int> vertexNdx;
    vector<int> uvNdx;
    vector<int> normalNdx;
    vector<vec3> tempVertices;
    vector<vec2> tempUvs;
    vector<vec3> tempNormals;

    string objPrefix = "";
    string trash = "";

    vec3 vertex;
    vec3 normal;
    vec2 uv;
    int vNdx[3];
    int uNdx[3];
    int nNdx[3];
    char dump;

    while (objFile >> objPrefix && objFile.good())
    {
        // Vertices.
        if (objPrefix == "v")
        {            
            objFile >> vertex.x >> vertex.y >> vertex.z;
            tempVertices.push_back(vertex);
        }
        // Normals.
        else if (objPrefix == "vn")
        {
            objFile >> normal.x >> normal.y >> normal.z;
            tempNormals.push_back(normal);
        }
        // UVs.
        else if (objPrefix == "vt")
        {
            objFile >> uv.x >> uv.y >> trash;
            tempUvs.push_back(uv);
        }
        // Faces.
        else if (objPrefix == "f")
        {
            for (int i = 0; i < 3; ++i)
            {
                objFile >> vNdx[i];
                objFile.get(dump);
                objFile >> uNdx[i];
                objFile.get(dump);
                objFile >> nNdx[i];
            }

            vertexNdx.push_back(vNdx[0]);
            vertexNdx.push_back(vNdx[1]);
            vertexNdx.push_back(vNdx[2]);

            uvNdx.push_back(uNdx[0]);
            uvNdx.push_back(uNdx[1]);
            uvNdx.push_back(uNdx[2]);

            normalNdx.push_back(nNdx[0]);
            normalNdx.push_back(nNdx[1]);
            normalNdx.push_back(nNdx[2]);
        }
        else
        {
            std::getline(objFile, trash);
        }
    }

    objFile.close();
    
    // Attributes respectively to indices.
    for (unsigned int i = 0; i < vertexNdx.size(); ++i)
    {
        vec3 vertex = tempVertices[vertexNdx[i] - 1];
        m_vertices.push_back(vertex);

        vec2 uv = tempUvs[uvNdx[i] - 1];
        m_uvs.push_back(uv);

        vec3 normal = tempNormals[normalNdx[i] - 1];
        m_normals.push_back(normal);
    }

    return true;
}

void Model::computeTangentBasis()
{
    vec3 deltam_vertices1;
    vec3 deltam_vertices2;

    vec2 deltaUv1;
    vec2 deltaUv2;

    for (unsigned int i = 0; i < m_vertices.size(); i += 3)
    {
        deltam_vertices1 = m_vertices[i+1] - m_vertices[i];
        deltam_vertices2 = m_vertices[i+2] - m_vertices[i];

        deltaUv1 = m_uvs[i+1] - m_uvs[i];
        deltaUv2 = m_uvs[i+2] - m_uvs[i];

        float r = 1.0f / (deltaUv1.x * deltaUv2.y - deltaUv1.y * deltaUv2.x);
        vec3 tangent = (deltam_vertices1 * deltaUv2.y - deltam_vertices2 * deltaUv1.y) * r;
        vec3 bitangent = (deltam_vertices2 * deltaUv1.x - deltam_vertices1 * deltaUv2.x) * r;

        // Values will be merged when attributes are indexed.
        m_tangents.push_back(tangent);
        m_tangents.push_back(tangent);
        m_tangents.push_back(tangent);

        m_bitangents.push_back(bitangent);
        m_bitangents.push_back(bitangent);
        m_bitangents.push_back(bitangent);
    }

    vec3 n;
    vec3 t;
    vec3 b;

    for (unsigned int i = 0; i < m_vertices.size(); ++i)
    {
        n = m_normals[i];
        t = m_tangents[i];
        b = m_bitangents[i];

        // Gram-Schmidt orthogonalization.
        t = glm::normalize(t - n * glm::dot(n, t));

        // Calculate handedness.
        if (glm::dot(glm::cross(n, t), b) < 0.0f)
        {
            t *= -1.0f;
        }

        m_tangents[i] = t;
    }
}

void Model::indexVBO()
{
    vector<unsigned short> indicesOut;
    vector<vec3> verticesOut;
    vector<vec3> normalsOut;
    vector<vec2> uvsOut;
    vector<vec3> tangentsOut;
    vector<vec3> bitangentsOut;

    map<Vertex, unsigned short> verticesCollector;

    for (unsigned int i = 0; i < m_vertices.size(); ++i)
    {
        Vertex v(m_vertices[i], m_normals[i], m_uvs[i]);
        map<Vertex, unsigned short>::iterator it = verticesCollector.find(v);

        if (it != verticesCollector.end())
        {
            indicesOut.push_back(it->second);
        }
        else
        {            
            verticesOut.push_back(m_vertices[i]);
            normalsOut.push_back(m_normals[i]);
            uvsOut.push_back(m_uvs[i]);
            tangentsOut.push_back(m_tangents[i]);
            bitangentsOut.push_back(m_bitangents[i]);

            unsigned short newIndex = static_cast<unsigned short>(verticesOut.size() - 1);
            indicesOut.push_back(newIndex);
            verticesCollector[v] = newIndex;
        }
    }

    m_indices = indicesOut;
    m_vertices = verticesOut;
    m_normals = normalsOut;
    m_uvs = uvsOut;
    m_tangents = tangentsOut;
    m_bitangents = bitangentsOut;
}

void Model::setVertexAttributes()
{
    glGenVertexArrays(1, &m_bufferObject.vao);
    glBindVertexArray(m_bufferObject.vao);

    glGenBuffers(1, &m_bufferObject.vertex);
    SetAttributeData<vector<vec3>, vec3>(m_vertices, m_bufferObject.vertex, 3, POSITION_ATTRIBUTE);

    glGenBuffers(1, &m_bufferObject.normal);
    SetAttributeData<vector<vec3>, vec3>(m_normals, m_bufferObject.normal, 3, NORMAL_ATTRIBUTE);

    glGenBuffers(1, &m_bufferObject.uv);
    SetAttributeData<vector<vec2>, vec2>(m_uvs, m_bufferObject.uv, 2, TEXCOORD_ATTRIBUTE);

    glGenBuffers(1, &m_bufferObject.tangent);
    SetAttributeData<vector<vec3>, vec3>(m_tangents, m_bufferObject.tangent, 3, TANGENT_ATTRIBUTE);

    glGenBuffers(1, &m_bufferObject.bitangent);
    SetAttributeData<vector<vec3>, vec3>(m_bitangents, m_bufferObject.bitangent, 3, BITANGENT_ATTRIBUTE);

    glGenBuffers(1, &m_bufferObject.element);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_bufferObject.element);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indices.size() * sizeof(unsigned short), &m_indices[0], GL_STATIC_DRAW);
}

template <class T, class U>
void Model::SetAttributeData(T& data, GLuint bufferNum, int size, GLuint attrib)
{
    glBindBuffer(GL_ARRAY_BUFFER, bufferNum);
    glBufferData(GL_ARRAY_BUFFER, data.size() * sizeof(U), &data[0], GL_STATIC_DRAW);
    
	glEnableVertexAttribArray(attrib);
	glVertexAttribPointer(attrib, size, GL_FLOAT, GL_FALSE, 0, 0);
}